<?php
/*
Quad_Orm
Copyright (c) 2009 Ken Snyder
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

/**
 * Simple ORM class
 */
class Quad_Orm extends Quad_MagicProperties {
	
	/**
	 * @var string  The name of the table
	 */
	protected $_table;
	
	/**
	 * @var array  List of table columns
	 */
	protected $_columns;

	/**
	 * @var string  The column(s) which define the primary key
	 */
	protected $_pk;
	
//	protected $_fk = array();
	
	protected $_serial;
	
	protected $_changed = array();
	
	public static $classPrefix = 'Orm_';
	
	/**
	 * Create object and optionally load from an id
	 * @param int [$id]  The primary key value to load
	 */
	public function __construct($id = null) {
		if ($id !== null) {
			$this->loadId($id);

		} elseif (is_array($id)) {
			$this->properties($id);
			
		} elseif ($id instanceof Quad_MagicProperties) {
			$this->properties($id->properties());
		}
	}
	
	public function __call($method, $args) {
		if (preg_match('/^(get|set|unset|isset)([A-Z]\w*)$/', $method, $match)) {
			list (, $type, $prop) = $match;
			$prop = self::_uncamelize($prop);
//			if (in_array($prop, $this->columns())) {
				switch ($type) {
					case 'get':
						return isset($this->_data[$prop]) ? $this->_data[$prop] : null;
	
					case 'set':
						$oldVal = $this->$prop;
			      $this->_data[$prop] = $args[0];
//			      if ($prop != $this->serial())
			      $this->_changed[$prop] = true;
			      $this->onChange($prop, $oldVal);
			      return $this;
								
					case 'unset':
						$oldVal = $this->$prop;						
			      unset($this->_data[$prop]);
			      $this->onChange($prop, $oldVal);
			      $this->_changed[$prop] = true;
			      return $this;
								
					case 'isset':
			      return isset($this->_data[$prop]);			
				}
//			}
		}
    $class = get_class($this);
    throw new Quad_Exception("Call to undefined function `$method` in class `$class`");
	}	
	
	/**
	 * Getters for private properties
   */	
	public function table() {
		return $this->_table;
	}
	
	public function columns() {
		return $this->_columns;
	}
	
	public function pk() {
		return $this->_pk;
	}
//	
//	public function fk($field) {
//		return (isset($this->_fk[$field]) ? $this->_fk[$field] : null);
//	}
//	
//	public function fks() {
//		return $this->_fk;
//	}
	
	public function serial() {
		return $this->_serial;
	}
	
	public function hasChanged($field = null) {
		if ($field === null) {
			// check for any changes
			return count($this->_changed);
		}
		// check for changes to that field
		return isset($this->_changed[$field]);
	}
	
	public function isDifferent($field, $value = null) {
		if (is_array($field)) {
			foreach ($field as $prop => $val) {
				if ($this->$prop != $val) {
					return true;
				}
			}
			return false;
		}
		return $this->$field != $value;
	}
	
	public function markAsChanged($field = null) {
		if ($field === null) {
			// mark all as changed
			$this->_changed = $this->_columns;
			foreach ($this->pk() as $pk) {
				unset($this->_changed[$pk]);
			}
		}
		else {
			// mark that column as changed
			$this->_changed[$field] = true;
		}
		$this->onChange($field, $this->$field);
		return $this;
	}
	
	public function __clone() {
		$ser = $this->serial();
		if ($ser && $this->$ser) {
			// remove the serial id so the row would be inserted anew
			$this->_data[$ser] = null;
			unset($this->_changed[$ser]);
		}
	}
	
	public function data() {
		$cols = $this->columns();
		$limited = array();
		foreach ($this->_data as $field => $value) {			
			if (in_array($field, $cols)) {
				$limited[$field] = $this->$field;
			}
		}		
		return $limited;
	}
	
	/**
	 * Reduce session footprint; php will automagically repopulate the values on wakeup
	 */
	public function __sleep() {
		$props = array_keys(get_object_vars($this));
		foreach ($props as $i => $name) {
			if (in_array($name, array('_table', '_columns', '_pk', '_serial'))) {
				unset($props[$i]);
			}
		}
		$this->preSleep($props);
		return $props;
	}
	
	public function preSleep(&$props) {}
	
	public function db() {
		return Quad_Db::get();		
	}
	
	public function tableModel() {
		static $model;
		if (!$model) {
			$model = new Quad_Db_Table($this->table(), $this->db());
		}
		return $model;
	}
	
	/**
	 * Insert or update record to database
	 * @return int|bool  New auto-increment value on insert or true on update
	 */
	public function save() {
//Quad_Debug::dump($this);			
		// nothing changed
		if (count($this->_changed) == 0) {
			$this->onSave(true);
			return true;
		}
		// update record
		if ($this->isInDb()) {
			$set = array();
			foreach ($this->columns() as $field) {
				if (isset($this->_changed[$field])) {
					$set[$field] = $this->$field;
				}
			}
			if (count($set) == 0) {
				// nothing changed
				$this->onSave(true);
				return true;
			}
			$where = array();
			foreach ($this->pk() as $col) {
				$where[$col] = $this->$col;
			}
//Quad_Debug::dumpdie($this->pk(), $where);			
			$affected = $this->tableModel()->update($set, $where);
			$success = ($affected !== false);
			if ($success) {
				$this->_changed = array();
			}
else Quad_Debug::log('update error', $this->db()->errorInfo());
			$this->onSave($success);
			return $success;	
		}
		// insert record
		else {
			$data = $this->data();
			$ok = $this->tableModel()->insert($data);
			if ($ok) {
				$this->_changed = array();
				if (($ser = $this->serial())) {
					$this->$ser = $ok;
				}				
			}
else Quad_Debug::log('insert error', $this->db()->errorInfo());	
			$this->onSave($ok);
			return $ok;
		}
	}
	
	public function onSave($result) {}
	
	public function onChange($property, $oldValue) {}
	
	public function delete($keepProperties = false, $keepSerial = false) {
		$ok = true;
		if ($this->isInDb()) {
			$where = array();
			foreach ($this->pk() as $col) {
				$where[$col] = $this->$col;
			}			
			$ok = $this->tableModel()->delete($where);
		}
		if ($this->keepProperties) {
			// clear only the serial
			if (!$keepSerial && ($ser = $this->serial())) {
				$this->$ser = null;
			}
		}
		else {
			$this->clear();
			$this->_changed = array();
		}
		return $ok;
	}
	
	/**
	 * Check if item is yet in DB
	 */
	public function isInDb(/*$recheck = false*/) {		
//$ser = $this->serial(); Quad_Debug::dumpdie($ser, $this->$ser, $this->properties());			
		if (($ser = $this->serial())) {
			return (int) $this->$ser > 0;
		}
		$val = '';
		foreach ($this->pk() as $col) {
//			if ($this->$col === null) {
//				return false;
//			}
			$val .= (string) $this->$col;
		}
//Quad_Debug::dumpdie($this->pk, $val);		
		return strlen($val) > 0; 
	}
	
	/**
	 * Load data from primary key values
	 * @param int $id
	 * @return bool  True if found in db
	 */
	public function loadId($id = null) {
		if ($id === null) {
			$where = array();
			foreach ($this->pk() as $col) {
				$where[$col] = $this->$col;
			}
		}
		elseif (is_array($id)) {
			$where = $id;
		}
		else {
			$where = array();
			$pk = $this->pk();
			$where[$pk[0]] = $id;
		}
//Quad_Debug::dump($where);		
		return $this->load($where);
	}
	
	/**
	 * Reload from database
	 * @return bool
	 */
	public function revert() {
		return $this->loadId();
	}
	
	/**
	 * Load data from criteria
	 * @param array $criteria  Field-value pairs to add to the where clause
	 * @return bool  True if found in
	 */
	public function load($criteria = null) {
		if ($criteria === null) {
			$queryCriteria = $this->data();
		}
		else {
			$queryCriteria = array();
			foreach ($this->columns() as $col) {
				if (array_key_exists($col, $criteria)) {
					$queryCriteria[$col] = $criteria[$col];
				}
			}
		}
		$fullRecord = $this->tableModel()->selectFirst('*', $queryCriteria);
//Quad_Debug::dumpdie('load!', $queryCriteria, $fullRecord);
		if ($fullRecord) {
			$this->properties($fullRecord);
			return true;
		}
		return false;
	}
	
	public function quadDebug() {
		$cols = $this->columns();
		$props = get_object_vars($this);
		unset($props['_table']);
		unset($props['_columns']);
		unset($props['_pk']);
		unset($props['_fk']);
		unset($props['_serial']);
		unset($props['_observers']);
		unset($props['_notifyObservers']);
		foreach ($props['_data'] as $p => $v) {
			if (!in_array($p, $cols)) {
				$p .= '*';
			}
			$props[$p] = $v;
		}
		unset($props['_data']);
		$chg = join(',',array_keys($this->_changed));
		unset($props['_changed']);
		$props['_changed'] = $chg;
		return $props;
	}
		
//	/**
//	 * Get a collection of orm objects that are "children" of this record
//	 * @param string $foreignTable
//	 * @param string $foreignColumn
//	 * @param string [$recordField]  The name of the field in this record to record
//	 * @return Orm_Collection
//	 */
//	public function findRelatedCollection($foreignTable, $foreignColumn, $recordField = null) {
//		if (!$recordField) {
//			$recordField = $foreignColumn;
//		}
//		$sql = "SELECT * FROM $foreignTable WHERE $foreignColumn = :value";
//		$values = array('value' => $this->$recordField);
//		$rs = $this->db()->select($sql, $values);
//		$collection = new Quad_Orm_Collection($foreignTable);
//		$collection->setRecordset($rs);
//		return $collection;
//	}
//
//	public function findRelatedRecord($foreignTable, $foreignColumn, $recordField = null) {
//		if (!$recordField) {
//			$recordField = $foreignColumn;
//		}
//		$sql = "SELECT * FROM $foreignTable WHERE $foreignColumn = :value";
//		$values = array('value' => $this->$recordField);
//		$rs = $this->db()->selectFirst($sql, $values);
//		$record = self::get($foreignTable);
//		$record->properties($rs);
//		return $record;
//	}
//	
//	public function __call($m, $a) {
//		if (preg_match('/^related([A-Z]\w+)(Collection)?$/', $m, $match)) {
//			$recordField = self::_uncamelize($match[1]);
//			$toMany = (bool) $match[2];
//			$relationship = $this->fk($recordField);
//			if ($relationship) {
//				list ($foreignTable,$foreignColumn) = explode('.', $relationship);
//				if ($toMany) {
//					$result = $this->findRelatedCollection($foreignTable, $foreignColumn, $recordField);
//				}
//				else {
//					$result = $this->findRelatedRecord($foreignTable, $foreignColumn, $recordField);
//				}
//				return $result; 
//			}
//		}
//		return parent::__call($m, $a);
//	}
//	
//	public function relateTo(Quad_Orm $record) {
//		// record and save() together?
//	}
	
	public static function get($tableName, $id = null) {
		$phpClass = self::getClassName($tableName);
		return new $phpClass($id);
	}
	
	public static function getClassName($tableName) {
		$prefix = self::$classPrefix;
		$suffix = ucwords(preg_replace('/_([a-z])/e', 'strtoupper("$1")', $tableName));
		return $prefix . $suffix;
	}
	
	public static function callStatic($tableName, $method/*[,$arg1][,$arg2]*/) {
		$args = func_get_args();
		$tableName = array_shift($args);
		$method = array_shift($args);
		$phpClass = self::getClassName($tableName);
		return call_user_func_array(array($phpClass, $method), $args);
	}

	public static function applyStatic($tableName, $method, $args) {
		$phpClass = self::getClassName($tableName);
		return call_user_func_array(array($phpClass, $method), $args);
	}
		
}